package com.jsftoolkit.gen;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.xml.sax.SAXException;

import com.jsftoolkit.gen.builder.ComponentBuilder;
import com.jsftoolkit.gen.config.ConfigurationUpdater;
import com.jsftoolkit.gen.info.ClassInfo;
import com.jsftoolkit.gen.info.ComponentInfo;
import com.jsftoolkit.gen.info.RendererInfo;
import com.jsftoolkit.gen.info.impl.TemplateComponentInfoProvider;
import com.jsftoolkit.gen.writer.impl.ComponentWriterImpl;
import com.jsftoolkit.gen.writer.impl.RendererWriterImpl;
import com.jsftoolkit.gen.writer.impl.TagWriterImpl;
import com.jsftoolkit.utils.DeferedFileOutputStream;
import com.jsftoolkit.utils.Utils;

/**
 * This is a collection of utility methods for generating code. The intention of
 * these methods is to hide the details of writing our files, instantiating code
 * generators, etc. so most methods take only an instance of
 * {@link ComponentInfo}.
 * 
 * @author noah
 * 
 */
public class GenerateUtils {

	/**
	 * Fills info with data gathered from the template text. A
	 * {@link TemplateComponentInfoProvider} instance is created for the
	 * template and used to fill in the information.
	 * 
	 * @param templateText
	 * @param info
	 * @throws IOException
	 * @throws SAXException
	 */
	public static void fillInFromTemplate(String templateText,
			ComponentInfo info) throws IOException, SAXException {
		if (templateText != null) {
			new TemplateComponentInfoProvider(templateText).fillIn(info);
		}
	}

	/**
	 * Writes out the component class.
	 * 
	 * @param info
	 * @throws SecurityException
	 * @throws IOException
	 */
	public static void generateComponent(ComponentInfo info) {
		new ComponentGeneratorImpl().generate(info, new ComponentWriterImpl(
				defaultStream(info)));
	}

	/**
	 * Writes out the component tag handler.
	 * 
	 * @param info
	 * @throws SecurityException
	 * @throws IOException
	 */
	public static void generateTag(ComponentInfo info) {
		new TagGeneratorImpl().generate(info, new TagWriterImpl(
				defaultStream(info.getTag())));
	}

	/**
	 * Writes out the component's renderer.
	 * 
	 * @param info
	 * @throws SecurityException
	 * @throws IOException
	 */
	public static void generateRenderer(ComponentInfo info) {
		new RendererGeneratorImpl().generate(info, new RendererWriterImpl(
				defaultStream(info.getRenderer())));
	}

	/**
	 * Calls {@link #defaultStream(String, String)} with the package and class
	 * name from info.
	 * 
	 * @param info
	 * @return
	 * @throws SecurityException
	 * @throws IOException
	 */
	public static PrintStream defaultStream(ClassInfo info) {
		return defaultStream(info.getPackage(), info.getClassName());
	}

	/**
	 * Creates a print stream for the {@link File} returned by
	 * {@link #getFile(String, String)}, creating the parent directories if
	 * necessary. If, for some reason that doesn't throw an exception, the file
	 * cannot be created, null is returned.
	 * 
	 * @param _package
	 * @param className
	 * @return
	 * @throws SecurityException
	 * @throws IOException
	 */
	public static PrintStream defaultStream(String _package, String className) {
		File file = getFile(_package, className);
		if (!file.exists()) {
			if (!Utils.createFileParents(file)) {
				return null;
			}
		}
		System.out.println("Writing to: " + file);
		return new PrintStream(new DeferedFileOutputStream(file));
	}

	/**
	 * Creates a {@link File} representing the expected location of the source
	 * file for the given class (package+className).
	 * <p>
	 * Two system properties determine the behavior of this method.
	 * <dl>
	 * <dt>jsftoolkit.generator.basedir
	 * <dd>The base directory for paths. Defaults to "" which means the current
	 * working directory.
	 * <dt>jsftoolkit.generator.srcDir
	 * <dd>Appended to basedir. Defaults to
	 * "target/generated-sources/components/"
	 * </dl>
	 * 
	 * @param _package
	 * @param className
	 * @return
	 */
	public static File getFile(String _package, String className) {
		String prefix = System.getProperty("jsftoolkit.generator.basedir", "");
		String srcDir = System.getProperty("jsftoolkit.generator.srcDir",
				"target/generated-sources/components/");
		prefix = fixupDirectory(prefix);
		srcDir = fixupDirectory(srcDir);
		return new File(prefix + srcDir + _package.replaceAll("\\.", "/") + '/'
				+ className + ".java");
	}

	/**
	 * Ensures that the path ends with the file separator character.
	 * 
	 * @param path
	 * @return
	 */
	public static String fixupDirectory(String path) {
		if (!Utils.isEmpty(path) && !path.endsWith(File.pathSeparator)) {
			return path + '/';
		}
		return path;
	}

	public static void updateAll(ComponentInfo... infos) throws Exception {
		updateAll(new Utils.ArrayIterable<ComponentInfo>(infos));
	}

	/**
	 * Updates all the configuration files for the given components.
	 * 
	 * @param infos
	 * @throws Exception
	 */
	public static void updateAll(Iterable<ComponentInfo> infos)
			throws Exception {
		Iterator<Set<ComponentInfo>> factory = new Utils.StaticIterator<Set<ComponentInfo>>() {
			public boolean hasNext() {
				return true;
			}

			public Set<ComponentInfo> next() {
				return new LinkedHashSet<ComponentInfo>();
			}

		};
		// lump the components together by file so we only have to read/write
		// each file once
		Map<String, Set<ComponentInfo>> facesConfigs = new LinkedHashMap<String, Set<ComponentInfo>>();
		Map<String, Set<ComponentInfo>> taglibs = new LinkedHashMap<String, Set<ComponentInfo>>();
		Map<String, Set<ComponentInfo>> tlds = new LinkedHashMap<String, Set<ComponentInfo>>();
		for (ComponentInfo info : infos) {
			Utils.getValue(facesConfigs, info.getConfig().getFacesConfig(),
					factory).add(info);
			Utils.getValue(taglibs, info.getConfig().getTaglibXml(), factory)
					.add(info);
			Utils.getValue(tlds, info.getConfig().getTldFile(), factory).add(
					info);
		}

		// update the files
		ConfigurationUpdater updater = new ConfigurationUpdater();
		// update each faces config
		for (Entry<String, Set<ComponentInfo>> entry : facesConfigs.entrySet()) {
			String fname = entry.getKey();
			if (fname == null) {
				// skip null files
				continue;
			}
			updater.updateFacesConfig(new File(fname), entry.getValue()
					.toArray(new ComponentInfo[entry.getValue().size()]));
		}
		// update each taglib.xml file
		for (Entry<String, Set<ComponentInfo>> entry : taglibs.entrySet()) {
			ComponentInfo[] array = entry.getValue().toArray(
					new ComponentInfo[entry.getValue().size()]);
			String fname = entry.getKey();
			if (fname == null) {
				// skip null files
				continue;
			}
			try {
				// XXX the exception that is sometime thrown here is caused by
				// the
				// code that reads the DTD declaration trying to instantiate a
				// URL object with null. No luck yet in finding out why
				updater.updateTaglibXml(array[0].getConfig().getNamespace(),
						new File(fname), array);
			} catch (MalformedURLException e) {
				System.err
						.println("MalformedURlException updating file: "
								+ fname
								+ ".\n"
								+ "If you generate again (w/o cleaning), it should be fine.");
			}
		}
		// update each TLD file
		for (Entry<String, Set<ComponentInfo>> entry : tlds.entrySet()) {
			String fname = entry.getKey();
			if (fname == null) {
				// skip null files
				continue;
			}
			ComponentInfo[] array = entry.getValue().toArray(
					new ComponentInfo[entry.getValue().size()]);
			updater.updateTld(array[0].getConfig().getNamespace(), array[0]
					.getConfig().getLibraryShortName(), new File(fname), array);
		}
	}

	/**
	 * Generates the all the code and updates all the configuration files for
	 * the given components. Assumes that if a component info instance has a
	 * non-empty renderer type, and non-null template or head template
	 * specified, that a {@link TemplateComponentInfoProvider} should be created
	 * and applied to the instance and that a renderer should be generated.
	 * 
	 * @param infos
	 *            the component info instance(s) to use
	 * @throws Exception
	 */
	public static void genAndUpdateAll(ComponentInfo... infos) throws Exception {
		genAndUpdateAll(new Utils.ArrayIterable<ComponentInfo>(infos));
	}

	public static void genAndUpdateAll(ComponentBuilder... builders)
			throws Exception {
		ComponentInfo[] infos = new ComponentInfo[builders.length];
		int i = 0;
		for (ComponentBuilder builder : builders) {
			infos[i++] = builder.getInfo();
		}
		genAndUpdateAll(infos);
	}

	/**
	 * Generates the renderer (if at least one template was specified),
	 * component class and tag handler. If any templates were specified,
	 * {@link #fillInFromTemplate(String, String, ComponentInfo)} will be called
	 * for each of them.
	 * 
	 * @param info
	 * @throws IOException
	 * @throws SAXException
	 */
	public static void generateAll(ComponentInfo info) throws IOException,
			SAXException {
		RendererInfo renderer = info.getRenderer();
		List<String> templates = renderer.getTemplates();
		if (!Utils.isEmpty(info.getRendererType()) && templates != null
				&& !templates.isEmpty()) {
			fillInFromTemplates(info);
			generateRenderer(info);
		}
		if (info.getPackage() != null && info.getClassName() != null) {
			generateComponent(info);
		}
		// always need a tag
		generateTag(info);
	}

	public static void genAndUpdateAll(Iterable<ComponentInfo> infos)
			throws Exception {
		for (ComponentInfo info : infos) {
			try {
				generateAll(info);
			} catch (Exception e) {
				throw new RuntimeException("Error generating classes for " + info, e);
			}
		}
		updateAll(infos);
	}

	/**
	 * Calls {@link #fillInFromTemplate(String, String, ComponentInfo)} for each
	 * template that info has.
	 * 
	 * @param info
	 * @throws IOException
	 * @throws SAXException
	 */
	public static void fillInFromTemplates(ComponentInfo info)
			throws IOException, SAXException {
		List<String> templates = info.getRenderer().getTemplates();
		String templateEncoding = info.getRenderer().getTemplateEncoding();
		if (!templates.isEmpty()) {
			StringBuilder template = new StringBuilder();
			for (String piece : templates) {
				template
						.append(Utils.resourceToString(piece, templateEncoding));
			}
			fillInFromTemplate(template.toString(), info);
		}
	}

}
